package com.example.mq2.mqserver.datacenter;

import com.example.mq2.common.BinaryTool;
import com.example.mq2.common.MqException;
import com.example.mq2.mqserver.core.MSGQueue;
import com.example.mq2.mqserver.core.Message;
import org.slf4j.spi.MDCAdapter;

import java.io.*;
import java.util.*;

/**
 * 管理消息文件
 */
public class MessageFileManager {

    public void init() {
    }

    /**
     * 数据统计文件
     */
    public static class Stat{
        public int totalCount;
        public int validCount;
    }

    /**
     * 获取队列文件夹
     * @param queueName
     * @return
     */
    public String getQueueDirPath(String queueName) {
        return "./data/" + queueName;
    }

    /**
     * 获取队列数据文件
     * @param queueName
     * @return
     */
    public String getQueueDataFilePath(String queueName) {
        return getQueueDirPath(queueName) + "/queue_data.txt";
    }

    /**
     * 获取队列统计文件
     * @param queueName
     * @return
     */
    public String getQueueStatFilePath(String queueName) {
        return getQueueDirPath(queueName) + "/queue_stat.txt";
    }


    /**
     * 创建消息相关文件
     * @param queueName
     * @throws IOException
     */
    public void createQueueFile(String queueName) throws IOException {
        File queueDir = new File(getQueueDirPath(queueName));
        if(!queueDir.exists()) {
            boolean ok1 = queueDir.mkdirs();
            if(!ok1) {
                throw new IOException("[MessageFileManager] 队列文件夹创建失败！path=" + queueDir.getAbsoluteFile());
            }
        }
        File statFile = new File(getQueueStatFilePath(queueName));
        if(!statFile.exists()){
            boolean ok2 = statFile.createNewFile();
            if(!ok2) {
                throw new IOException("[MessageFileManager] 队列统计文件创建失败！path=" + statFile.getAbsoluteFile());
            }
        }
        File dataFile = new File(getQueueDataFilePath(queueName));
        if(!dataFile.exists()) {
            boolean ok3 = dataFile.createNewFile();
            if(!ok3) {
                throw new IOException("[MessageFileManager] 队列数据文件创建失败！path=" + dataFile.getAbsoluteFile());
            }
        }
        //初始化队列统计文件
        Stat stat = new Stat();
        stat.totalCount = 0;
        stat.validCount = 0;
        writeStat(queueName, stat);
    }

    /**
     * 销毁队列文件
     * @param queueName
     */
    public void destroyQueueFile(String queueName) {
        File dataFile = new File(getQueueDataFilePath(queueName));
        boolean ok1 = dataFile.delete();
        File statFile = new File(getQueueStatFilePath(queueName));
        boolean ok2 = statFile.delete();
        File dirFile = new File(getQueueDirPath(queueName));
        boolean ok3 = dirFile.delete();
        if(!ok1 || !ok2 || !ok3) {
            System.out.println("[MessageFileManager] 队列相关文件删除失败！queueName=" + queueName);
        } else {
            System.out.println("[MessageFileManager] 队列相关文件删除成功！queueName=" + queueName);
        }
    }

    /**
     * 写队列统计文件
     * @param queueName
     * @param stat
     */
    public void writeStat(String queueName, Stat stat) {
        try (OutputStream outputStream = new FileOutputStream(getQueueStatFilePath(queueName))) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.write(stat.totalCount + "\t" + stat.validCount);
            printWriter.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 读取队列统计文件
     * @param queueName
     * @return
     */
    public Stat readStat(String queueName) {
        Stat stat = new Stat();
        try (InputStream inputStream = new FileInputStream(getQueueStatFilePath(queueName))) {
            Scanner scanner = new Scanner(inputStream);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
            return stat;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }



    /**
     * 将消息持久化到文件中
     * @param queue
     * @param message
     */
    public void sendMessage(MSGQueue queue, Message message) throws IOException {
        //1.检查队列文件是否存在
        if(!statFileExists(queue.getName())) {
            throw new IOException("[MessageFileManager] 消息持久化时，发现统计文件不存在！");
        }
        if(!dataFileExists(queue.getName())) {
            throw new IOException("[MessageFileManager] 消息持久化时，发现数据文件不存在！");
        }
        synchronized (queue) {
            //2.获取当前文件的长度，得出 offsetBeg 和 offsetEnd
            File dataFile = new File(getQueueDataFilePath(queue.getName()));
            long lengthFile = dataFile.length();
            byte[] payload = BinaryTool.toBytes(message);
            message.setOffsetBeg(lengthFile + 4);
            message.setOffsetEnd(lengthFile + 4 + payload.length);
            try (OutputStream outputStream = new FileOutputStream(getQueueDataFilePath(queue.getName()), true)) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    //3.按照约定的格式将消息写入文件
                    dataOutputStream.writeInt(payload.length);
                    dataOutputStream.write(payload);
                    dataOutputStream.flush();
                }
            }
            //4.更新统计文件
            Stat stat = readStat(queue.getName());
            stat.totalCount += 1;
            stat.validCount += 1;
            writeStat(queue.getName(), stat);
        }
    }

    /**
     * 删除消息
     * @param queue
     * @param message
     */
    public void deleteMessage(MSGQueue queue, Message message) throws IOException {
        //1.判断队列文件是否存在
        if(!statFileExists(queue.getName())) {
            throw new IOException("[MessageFileManager] 删除消息时，发现统计文件不存在！");
        }
        if(!dataFileExists(queue.getName())) {
            throw new IOException("[MessageFileManager] 删除消息时，发现数据文件不存在！");
        }
        synchronized (queue) {
            //2.将消息文件读取出来
            try (RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataFilePath(queue.getName()), "rw")) {
                byte[] payload = new byte[(int) (message.getOffsetEnd() - message.getOffsetBeg())];
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.read(payload);
                //3.将消息 isValid 设置为 0x0
                Message toDeleteMessage = (Message) BinaryTool.fromBytes(payload);
                toDeleteMessage.setIsValid((byte) 0x0);
                //4.将消息写入文件
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.write(BinaryTool.toBytes(toDeleteMessage));
            }
            //5.更新统计文件
            Stat stat = readStat(queue.getName());
            if(stat.validCount > 0) {
                stat.validCount -= 1;
            }
            writeStat(queue.getName(), stat);
        }
    }

    /**
     * 从队列文件中读取所有有效的消息
     * @param queue
     * @return
     * @throws IOException
     */
    public LinkedList<Message> loadAllMessageFromQueue(MSGQueue queue) throws IOException, MqException {
        //1.判断队列文件是否存在
        if(!statFileExists(queue.getName())) {
            throw new IOException("[MessageFileManager] 加载所有有效消息时，发现统计文件不存在！");
        }
        if(!dataFileExists(queue.getName())) {
            throw new IOException("[MessageFileManager] 加载所有有效消息时，发现数据文件不存在！");
        }
        synchronized (queue) {
            //2.从队列文件中读出所有有效的消息(注意要顺带设置 offset)
            LinkedList<Message> messages = new LinkedList<>();
            try (InputStream inputStream = new FileInputStream(getQueueDataFilePath(queue.getName()))) {
                try (DataInputStream dataInputStream = new DataInputStream(inputStream)) {
                    //当前读取文件的位置(用于计算 offset)
                    int index = 0;
                    while(true) {
                        int msgSize = dataInputStream.readInt();
                        byte[] messageBytes = new byte[msgSize];
                        int n = dataInputStream.read(messageBytes);
                        if(msgSize != n) {
                            throw new MqException("[MessageFileManager] 读取队列文件格式非法！expectedSize=" + msgSize +
                                    ", actualSize=" + n);
                        }
                        Message message = (Message) BinaryTool.fromBytes(messageBytes);
                        if(message.getIsValid() == 0x1) {
                            //有效文件
                            message.setOffsetBeg(index + 4);
                            message.setOffsetEnd(index + 4 + msgSize);
                            messages.add(message);
                            //更新位置
                            index = index + msgSize + 4;
                            continue;
                        }
                        //无效消息也要更新下标
                        index = index + msgSize + 4;
                    }
                }
            } catch (EOFException e) {
                System.out.println("[MessageFileManager] 所有有效消息文件读取完成！queueName=" + queue.getName());
            }
            return messages;
        }
    }

    /**
     * 检查 GC
     * 这里约定：当消息总数量大于 2000，并且有效消息小于总消息数的 50%，就进行 GC
     * @param queueName
     * @return
     */
    public boolean checkGC(String queueName) {
        Stat stat = readStat(queueName);
        if(stat.totalCount > 2000 && ((double) stat.validCount / stat.totalCount < 0.5)) {
            return true;
        }
        return false;
    }

    private String getNewDataFilePath(String queueName) {
        return getQueueDirPath(queueName) + "/queue_data_new.txt";
    }

    public void gc(MSGQueue queue) throws IOException, MqException {
        //gc 的过程相当于对消息文件的大洗牌，整个过程不能有其他线程干预操作队列消息文件
        synchronized (queue) {
            //可以记录一下 gc 的时间
            long startTime = System.currentTimeMillis();

            //1.判断队列文件是否存在
            if(!statFileExists(queue.getName())) {
                throw new IOException("[MessageFileManager] gc时，发现统计文件不存在！");
            }
            if(!dataFileExists(queue.getName())) {
                throw new IOException("[MessageFileManager] gc时，发现数据文件不存在！");
            }
            //2.创建一个新的文件
            File newDataFile = new File(getNewDataFilePath(queue.getName()));
            boolean ok = newDataFile.createNewFile();
            if(!ok) {
                throw new MqException("[MessageFileManager] gc时，新文件创建失败！path=" + newDataFile.getAbsoluteFile());
            }
            //3.将旧文件上所有有效消息写入到新文件中
            List<Message> messages = loadAllMessageFromQueue(queue);
            try (OutputStream outputStream = new FileOutputStream(getNewDataFilePath(queue.getName()))) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    for(Message message : messages) {
                        byte[] messageBytes = BinaryTool.toBytes(message);
                        dataOutputStream.writeInt(messageBytes.length);
                        dataOutputStream.write(messageBytes);
                        dataOutputStream.flush();
                    }
                }
            }
            //4.删除旧文件
            File oldDataFile = new File(getQueueDataFilePath(queue.getName()));
            ok = oldDataFile.delete();
            if(!ok) {
                throw new MqException("[MessageFileManager] gc时，旧文件删除失败！path=" + oldDataFile.getAbsoluteFile());
            }
            //5.将新文件改名为旧文件
            ok = newDataFile.renameTo(oldDataFile);
            if(!ok) {
                throw new MqException("[MessageFileManager] gc时，新文件更名失败！path=" + newDataFile.getAbsoluteFile());
            }
            //6.更新统计文件(总数 = 有效消息数)
            Stat stat = readStat(queue.getName());
            stat.totalCount = stat.validCount;
            writeStat(queue.getName(), stat);

            long endTime = System.currentTimeMillis();
            System.out.println("[MessageFileManager] gc完成！ 全程耗时:" + (endTime - startTime) + "ms");
        }
    }

    private boolean statFileExists(String queueName) {
        File statFile = new File(getQueueStatFilePath(queueName));
        return statFile.exists();
    }

    private boolean dataFileExists(String queueName) {
        File dataFile = new File(getQueueDataFilePath(queueName));
        return dataFile.exists();
    }


}
